const ctx = '@@InfiniteScroll'

var throttle = function (fn, delay) {
  var now, lastExec, timer, context, args; //eslint-disable-line

  var execute = function () {
    fn.apply(context, args)
    lastExec = now
  }

  return function () {
    context = this
    args = arguments

    now = Date.now()

    if (timer) {
      clearTimeout(timer)
      timer = null
    }

    if (lastExec) {
      var diff = delay - (now - lastExec)
      if (diff < 0) {
        execute()
      } else {
        timer = setTimeout(() => {
          execute()
        }, diff)
      }
    } else {
      execute()
    }
  }
}

var getScrollTop = function (element) {
  if (element === window) {
    return Math.max(window.pageYOffset || 0, document.documentElement.scrollTop)
  }

  return element.scrollTop
}

// getComputedStyle 读取的样式是最终样式，仅支持读并不支持写入
var getComputedStyle = document.defaultView.getComputedStyle

// 从自身开始，寻找设置了滚动的父元素。 overflow-y 为scroll或auto
var getScrollEventTarget = function (element) {
  var currentNode = element
  // bugfix, see http://w3help.org/zh-cn/causes/SD9013 and http://stackoverflow.com/questions/17016740/onscroll-function-is-not-working-for-chrome
  // nodeType 1表示元素节点
  while (currentNode && currentNode.tagName !== 'HTML' && currentNode.tagName !== 'BODY' && currentNode.nodeType === 1) {
    var overflowY = getComputedStyle(currentNode).overflowY
    if (overflowY === 'scroll' || overflowY === 'auto') {
      return currentNode
    }
    currentNode = currentNode.parentNode
  }
  return window
}

var getVisibleHeight = function (element) {
  if (element === window) {
    return document.documentElement.clientHeight
  }

  return element.clientHeight
}

var getElementTop = function (element) {
  if (element === window) {
    return getScrollTop(window)
  }
  // element.getBoundingClientRect().top 元素顶部相对视口顶部的相对距离
  return element.getBoundingClientRect().top + getScrollTop(window)
}

var isAttached = function (element) {
  var currentNode = element.parentNode
  while (currentNode) {
    if (currentNode.tagName === 'HTML') {
      return true
    }
    // nodeType === 11 表示节点是 DocumentFragment
    if (currentNode.nodeType === 11) {
      return false
    }
    currentNode = currentNode.parentNode
  }
  return false
}

var doBind = function () {
  // 只绑定一次，绑定过就返回
  if (this.binded) return
  this.binded = true

  var directive = this
  var element = directive.el

  // 截流间隔
  var throttleDelayExpr = element.getAttribute('infinite-scroll-throttle-delay')
  // 默认200毫秒
  var throttleDelay = 200
  if (throttleDelayExpr) {
    // 优先尝试实例上的throttleDelayExpr对应的属性
    throttleDelay = Number(directive.vm[throttleDelayExpr] || throttleDelayExpr)
    if (isNaN(throttleDelay) || throttleDelay < 0) {
      throttleDelay = 200
    }
  }
  directive.throttleDelay = throttleDelay

  directive.scrollEventTarget = getScrollEventTarget(element)
  directive.scrollListener = throttle(doCheck.bind(directive), directive.throttleDelay)
  directive.scrollEventTarget.addEventListener('scroll', directive.scrollListener)

  this.vm.$once('hook:beforeDestroy', function () {
    directive.scrollEventTarget.removeEventListener('scroll', directive.scrollListener)
  })

  // 是否禁用无限滚动
  var disabledExpr = element.getAttribute('infinite-scroll-disabled')
  // 默认不禁用
  var disabled = false

  // 如果配置了该项，则监听实例上的disabledExpr对应的属性
  if (disabledExpr) {
    this.vm.$watch(disabledExpr, function (value) {
      directive.disabled = value
      // 当disable为false时，重启check
      if (!value && directive.immediateCheck) {
        doCheck.call(directive)
      }
    })
    disabled = Boolean(directive.vm[disabledExpr])
  }
  directive.disabled = disabled

  // 滚动条与顶部或底部的距离阈值，小于这个值时候执行doCheck
  var distanceExpr = element.getAttribute('infinite-scroll-distance')
  // 默认为0
  var distance = 0
  if (distanceExpr) {
    distance = Number(directive.vm[distanceExpr] || distanceExpr)
    if (isNaN(distance)) {
      distance = 0
    }
  }
  directive.distance = distance

  // 触发类型是向上滚动还是向下滚动
  var triggerTypeExpr = element.getAttribute('infinite-scroll-trigger-type')
  // 默认触发类型为向下滚动
  var triggerType = 'scrollDown'
  if (triggerTypeExpr) {
    triggerType = directive.vm[triggerTypeExpr] || triggerTypeExpr
    if (!['scrollDown', 'scrollUp'].includes(triggerType)) {
      triggerType = 'scrollDown'
    }
  }
  directive.triggerType = triggerType

  // 是否立即执行doCheck
  var immediateCheckExpr = element.getAttribute('infinite-scroll-immediate')
  // 默认为true
  var immediateCheck = true
  if (immediateCheckExpr) {
    immediateCheck = Boolean(directive.vm[immediateCheckExpr])
  }
  directive.immediateCheck = immediateCheck

  if (immediateCheck) {
    doCheck.call(directive, false)
  }

  // 当组件上设置的此事件触发时，执行一次检查，一般用于手动触发检查
  var eventName = element.getAttribute('infinite-scroll-listen-for-event')
  if (eventName) {
    directive.vm.$on(eventName, function () {
      doCheck.call(directive)
    })
  }
}

var doCheck = function (force) {
  var scrollEventTarget = this.scrollEventTarget
  var element = this.el
  var distance = this.distance
  var triggerType = this.triggerType

  if (force !== true && this.disabled) return
  // 滚动元素顶部与文档坐标顶部的距离
  var viewportScrollTop = getScrollTop(scrollEventTarget)
  // viewportBottom： 滚动元素底部与文档坐标顶部的距离； visibleHeight：滚动元素不带边框的高度
  var viewportBottom = viewportScrollTop + getVisibleHeight(scrollEventTarget)
  // 是否触发
  var shouldTrigger = false

  // 滚动元素是宿主元素自身
  if (scrollEventTarget === element) {
    if (triggerType === 'scrollDown') {
      shouldTrigger = scrollEventTarget.scrollHeight - viewportBottom <= distance
    } else {
      shouldTrigger = viewportScrollTop <= distance
    }
  } else {
    // 滚动元素是宿主元素的父元素
    // 自身与父元素顶部距离差
    var topGap = getElementTop(element) - getElementTop(scrollEventTarget)
    // elementBottom：宿主元素底部与文档坐标顶部的距离
    var elementBottom = topGap + element.offsetHeight + viewportScrollTop

    if (triggerType === 'scrollDown') {
      shouldTrigger = viewportBottom + distance >= elementBottom
    } else {
      shouldTrigger = topGap <= distance
    }
  }

  if (shouldTrigger && this.expression) {
    this.expression()
  }
}

export default {
  bind (el, binding, vnode) {
    // 官方警告：除了 el 之外，其它参数都应该是只读的，切勿进行修改。如果需要在钩子之间共享数据，建议通过元素的 dataset 来进行。
    el[ctx] = {
      el,
      vm: vnode.context, // vue实例
      expression: binding.value // 滚动到底部或顶部时需要的回调函数，通常用于加载下一页数据
    }
    const args = arguments
    el[ctx].vm.$once('hook:mounted', function () {
      el[ctx].vm.$nextTick(function () {
        // 判断元素是否已经在页面上
        if (isAttached(el)) {
          doBind.call(el[ctx], args)
        }

        el[ctx].bindTryCount = 0

        // 间隔50ms轮询10次，判断元素是否已经在页面上
        var tryBind = function () {
          if (el[ctx].bindTryCount > 10) return
          el[ctx].bindTryCount++
          if (isAttached(el)) {
            doBind.call(el[ctx], args)
          } else {
            setTimeout(tryBind, 50)
          }
        }

        tryBind()
      })
    })
  },

  unbind (el) {
    // 滚动事件解绑
    if (el && el[ctx] && el[ctx].scrollEventTarget) { el[ctx].scrollEventTarget.removeEventListener('scroll', el[ctx].scrollListener) }
  }
}
